﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ExecutePowershell.cs" company="Collaboris Ltd.">
//   Copyright (c) Collaboris Ltd. All rights Reserved.
// </copyright>
// <summary>
//   Allows Powershell Commands to be called from Saf.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Collaboris.Saf.Actions.Powershell
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.IO;
    using System.Management.Automation;
    using System.Management.Automation.Runspaces;
    using System.Text;


    using Engine;

    using Entities;

    using SharePoint.Wss.Attributes;

    using Utils;

    /// <summary>
    /// Run a Powershell Command.
    /// </summary>
    [CollaborisClass(
        Title = "Runs Powershell Scripts.",
        ReleaseStatus = ReleaseStatus.Stable,
        DisableDocumentation = false,
        ContactAuthorEmail = "Support@Collaboris.co.uk",
        Summary = "Allows 1 (or more) Powershell scripts to be run.",
        UndoImplemented = true,
        ClassGuid = "{156E17A5-A7BA-4e1f-8A48-1BC341A7C611}"
        )
    ]
    public class ExecutePowershell : ActionBase<ExternalEngineWrapper<PowershellScript>>, IPowershellAction
    {
        /// <summary>
        /// host.
        /// </summary>
        private PowershellHost host;
        private PowershellScript currentScript;

        /// <summary>
        /// Gets or sets the exit code.
        /// </summary>
        /// <value>The exit code.</value>
        public int ExitCode
        {
            get
            {
                throw new NotImplementedException();
            }

            set
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether [should exit].
        /// </summary>
        /// <value><c>true</c> if [should exit]; otherwise, <c>false</c>.</value>
        public bool ShouldExit
        {
            get
            {
                throw new NotImplementedException();
            }

            set
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// Executes the Action using the values passed in by the Data Entity
        /// and the <see cref="SafContext"/>.
        /// </summary>
        /// <param name="context">
        /// A valid <see cref="SafContext"/> containing properties to run the Action with.
        /// </param>
        /// <param name="entity">
        /// A Valid <see cref="T"/> containing the parameters with which to run the Action with.
        /// </param>
        /// <returns>
        /// A <see cref="ProcessStatus"/> signifying the outcome of processing the Action.
        /// </returns>
        protected override ProcessStatus DoAction(SafContext context, ExternalEngineWrapper<PowershellScript> entity)
        {
            this.currentScript = entity.DoScript;

            if (this.currentScript == null)
                return ProcessStatus.Skip;

            return this.RunPowershellScript(this.currentScript);
        }

        /// <summary>
        /// Undoes the action.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <param name="entity">The entity.</param>
        /// <returns></returns>
        protected override ProcessStatus UndoAction(SafContext context, ExternalEngineWrapper<PowershellScript> entity)
        {
            this.currentScript = entity.UndoScript;

            if (this.currentScript == null)
                return ProcessStatus.Skip;

            return this.RunPowershellScript(this.currentScript);
        }

        /// <summary>
        /// Checks the Data entity used by the Action for Validity
        /// </summary>
        /// <param name="context">
        /// The <see cref="SafContext"/> passed in by the SAF Engine.
        /// </param>
        /// <param name="entity">
        /// The data entity to Validate.
        /// </param>
        /// <param name="errors">
        /// List of <see cref="ValidationError"/>s
        /// </param>
        /// <param name="mode">
        /// <see cref="ProcessMode"/>signalling wheter we are Doing, or Undoing the action.
        /// </param>
        /// <returns>
        /// false if invalid; otherwise true.
        /// </returns>
        protected override bool IsValid(
            SafContext context, ExternalEngineWrapper<PowershellScript> entity, ref List<ValidationError> errors, ProcessMode mode)
        {
            TraceHelper.TraceMethodStart(TraceSwitch, "Powershell", "IsValid");

            this.currentScript = entity.DoScript;

            int currentCount = errors.Count;


            if (!string.IsNullOrEmpty(this.currentScript.ScriptFile) && !string.IsNullOrEmpty(this.currentScript.ScriptText))
            {
                errors.Add(
                    new ValidationError(
                        "PowershellScript.ScriptText / PowershellScript.ScriptFile",
                        "A value for 'ScriptFile' and 'ScriptText' has been supplied. "
                        + "Only one can be populated."));
            }

            // loop through and validate all the keys and Values
            foreach (string command in this.currentScript.Commands)
            {
                ValidateStringForNull(command, "PowershellScript.Command", ref errors);
            }

            TraceHelper.TraceMethodStart(TraceSwitch, "Powershell", "IsValid");

            // Have we added errors ?
            return errors.Count == currentCount;
        }

        /// <summary>
        /// Sets the variable if not null.
        /// </summary>
        /// <param name="runspace">
        /// The runspace.
        /// </param>
        /// <param name="variable">
        /// The variable.
        /// </param>
        /// <param name="value">
        /// The value.
        /// </param>
        private static void SetVariableIfNotNull(Runspace runspace, string variable, object value)
        {
            TraceHelper.TraceMethodStart(TraceSwitch, "Powershell", "SetVariableIfNotNull");

            if (value != null)
            {
                runspace.SessionStateProxy.SetVariable(variable, value);
            }

            TraceHelper.TraceMethodStart(TraceSwitch, "Powershell", "SetVariableIfNotNull");
        }

        /// <summary>
        /// append commands.
        /// </summary>
        /// <param name="entity">
        /// The entity.
        /// </param>
        /// <param name="sb">
        /// The sb.
        /// </param>
        private void AppendCommands(PowershellScript entity, StringBuilder sb)
        {
            foreach (string command in entity.Commands)
            {
                this.Trace.TraceVerbose("Preparing to run this command in Powershell : '{0}'", command);
                sb.AppendLine(command);
            }
        }

        /// <summary>
        /// Fills up the global variables available to the script.
        /// </summary>
        /// <param name="runspace">
        /// The runspace.
        /// </param>
        /// <param name="entity">
        /// The entity.
        /// </param>
        private void FillVariables(Runspace runspace, PowershellScript entity)
        {
            TraceHelper.TraceMethodStart(TraceSwitch, "Powershell", "FillVariables");

            SetVariableIfNotNull(runspace, "VerbosePreference", entity.VerbosePreference);
            SetVariableIfNotNull(runspace, "DebugPreference", entity.DebugPreference);
            SetVariableIfNotNull(runspace, "ErrorActionPreference", entity.ErrorActionPreference);
            SetVariableIfNotNull(runspace, "ShouldProcessPreference", entity.ShouldProcessPreference);
            SetVariableIfNotNull(runspace, "ProcessReturnPreference", entity.ProcessReturnPreference);

            TraceHelper.TraceMethodEnd(TraceSwitch, "Powershell", "FillVariables");
        }

        /// <summary>
        /// Adds the script file.
        /// </summary>
        /// <param name="entity">
        /// The entity.
        /// </param>
        /// <returns>
        /// The get script contents.
        /// </returns>
        private string GetScriptContents(PowershellScript entity)
        {
            TraceHelper.TraceMethodStart(TraceSwitch, "Powershell", "GetScriptContents");
            if (!string.IsNullOrEmpty(entity.ScriptFile))
            {
                this.Trace.TraceVerbose(
                    "The powershell command has been supplied as File called '{0}'.", entity.ScriptFile);

                if (! File.Exists(entity.ScriptFile))
                {
                    throw new FileNotFoundException("Cannot Find this script file.", entity.ScriptFile);
                }

                string script = File.ReadAllText(entity.ScriptFile);
                return script;
            }

            if (!string.IsNullOrEmpty(entity.ScriptText))
            {
                this.Trace.TraceVerbose("The powershell command has been supplied as Text in the Macro.");

                string script = entity.ScriptText;
                return script;
            }

            TraceHelper.TraceMethodEnd(TraceSwitch, "Powershell", "GetScriptContents");
            return string.Empty;
        }

        /// <summary>
        /// Invokes the PS Script.
        /// </summary>
        /// <param name="runspace">
        /// The runspace.
        /// </param>
        /// <param name="entity">
        /// The entity.
        /// </param>
        /// <returns>
        /// A Collection of PSObjects
        /// </returns>
        private Collection<PSObject> InvokeScript(Runspace runspace, PowershellScript entity)
        {
            Collection<PSObject> powerShellResults;
            using (var scriptInvoker = new RunspaceInvoke(runspace))
            {
                string script = this.GetScriptContents(entity);
                var sb = new StringBuilder();
                if (!string.IsNullOrEmpty(script))
                {
                    sb.AppendLine(script);
                    this.Trace.TraceVerbose("Invoking this ScriptText '{0}'", script);
                }
                else
                {
                    this.Trace.TraceVerbose(
                        "No ScriptFile or ScriptText has been supplied. Attempting to run the Command without.");
                }

                this.AppendCommands(entity, sb);

                powerShellResults = scriptInvoker.Invoke(sb.ToString());
            }

            return powerShellResults;
        }

        /// <summary>
        /// run powershell script.
        /// </summary>
        /// <param name="entity">
        /// The entity.
        /// </param>
        /// <returns>
        /// </returns>
        private ProcessStatus RunPowershellScript(PowershellScript entity)
        {
            this.Trace.TraceMethodStart("RunPowershellScript");

            this.Trace.TraceVerbose("Creating the Powershell Runspace Configuration.");

            Collection<PSObject> powerShellResults;

            this.host = new PowershellHost(this);
            Pipeline pipeline;

            // Create the runspace and stuff.
            Runspace runspace = RunspaceFactory.CreateRunspace(this.host);
            {
                TraceHelper.TraceVerbose(TraceSwitch, "Powershell", "Creating Pipeline");
                pipeline = runspace.CreatePipeline();

                this.SetUpOutputStreams(pipeline);

                this.Trace.TraceVerbose("Opening a new Powershell RunSpace");
                runspace.Open();

                this.FillVariables(runspace, entity);

                // Running the ScriptText
                powerShellResults = this.InvokeScript(runspace, entity);
            }
            
            foreach (PSObject obj in powerShellResults)
            {
                this.Trace.TraceInfo(StringHelper.EscapeForFormat(obj.ToString()));
            }

            // Did it complete ok?
            PipelineStateInfo info = pipeline.PipelineStateInfo;
            // TODO: Need to check the status. But the below lines are commented because the pipeline is not 
            // invoked and the status remains the same.
            // if (info.State != PipelineState.Completed)
            // {
            //    throw new ApplicationException("Execute Powershell has failed.", info.Reason);
            // }

            // return the output items as strings.
            if (powerShellResults != null)
            {
                var outputObjects = new PSObject[powerShellResults.Count];
                powerShellResults.CopyTo(outputObjects, 0);

                // this.Reports = Array.ConvertAll<PSObject, ITaskItem>(outputObjects,
                // psObject =>
                // psObject != null
                // ? new TaskItem(psObject.ToString())
                // : new TaskItem(String.Empty));
            }

            

            // TODO : Use List some how
            this.Trace.TraceMethodEnd("RunPowershellScript");
            return ProcessStatus.Success;
        }

        /// <summary>
        /// Sets up output streams.
        /// </summary>
        /// <param name="pipeline">
        /// The pipeline.
        /// </param>
        private void SetUpOutputStreams(Pipeline pipeline)
        {
            TraceHelper.TraceMethodStart(TraceSwitch, "Powershell", "SetUpOutputStreams");

            // Write stuff piped to error to WriteErrorLine.
            pipeline.Error.DataReady += delegate
                {
                    for (int i = 0; i < pipeline.Error.Count; i++)
                    {
                        object obj = pipeline.Error.Read();
                        this.host.UI.WriteErrorLine(obj.ToString());
                    }
                };

            TraceHelper.TraceMethodEnd(TraceSwitch, "Powershell", "SetUpOutputStreams");
        }
    }
}